package com.frontalcode 
{
	import flash.events.*;
	import flash.utils.*;
	
	/**                  
     * The Scheduler class provides for frame- and time-based scheduled tasks.
     */
    public class Scheduler extends OptionableObject
    {
		static private var _instance : Scheduler = null;
		
		/*
			Variables: private variables
			
			These variables are not directly accessible.
			
			nextTaskId			- Used to track the next available id for a new 
								  scheduled task.
			taskIndexes			- A map from a task id to its index in the scheduled 
								  tasks array.
			tasks				- An array of scheduled tasks.
			runningNewTaskId	- Used in service of running a scheduled task as 
								  soon as it is created rather than waiting for the 
								  next frame.
			frameCount			- Used for those schedules that are based on frames 
								  rendered rather than wall-clock time.
		*/
		public var nextTaskId : uint = 1;
		private var taskIndexes : Object = { };
		private var tasks : Array = [ ];
		private var runningNewTaskId : uint = 0;
		private var frameCount : Number = 0;
	
		/**
		 * @private
		 */
		public function Scheduler ( options : Object = null )
		{
			super ( options );
			
			setOptionsFromLoaderParameters ( );
	
			_instance = this;
		}
		
		/*
			Function: getInstance
			
			Returns:
				
				An instance of Scheduler
		*/
		/**
		 * The single instance of the Scheduler class.
		 *
		 * @return The scheduler.
		 */
		static public function getInstance ( ) : Scheduler
		{
			if ( _instance == null ) throw new FrontalError ( "Scheduler not initialized" );
			return _instance;
		}
		
		// Convenience method.
		//
		/**
		 * The single instance of the Scheduler class.
		 *
		 * @return The scheduler.
		 */
		static public function gI ( ) : Scheduler { return getInstance ( ); }
		
		/*
			Function: getTaskTime
			
				Calculates time elapsed so far.
				
			Parameters:
			
				useFrameClock	- If false, calls getTimer. If true, returns time 
								  based on the <frameCount> and our frame rate.
								
			Returns:
			
				Elapsed time.
		*/
		/**            
		 * @private
		 */
		internal function getTaskTime ( task : Object ) : Number
		{
			if ( task.useFrames )
			{
				return frameCount;
			}
			else
			{
				if ( ! task.useFrameClock )
				{
					return getTimer ( );
				}
				else
				{
					return 1000 * frameCount / DocumentManager.getInstance ( ).fps;
				}
			}
		}
		
		/*
			Function: unitConversion
			
				Return the conversion from seconds to milliseconds if we're
				scheduled in seconds otherwise the conversion from frames to
				frames if we are scheduled in frames.
				
			Parameters:
			
				task	- the task of interest
								
			Returns:
			
				The conversion factor.
		*/
		/**            
		 * @private
		 */
		internal function unitConversion ( task : Object ) : Number
		{
			return task.useFrames ? 1 : 1000;
		}
		
		/*
			Function: setTimeout
			
				Run some function with arguments after a delay specified in
				seconds.
				
			Parameters:
			
				internal functionReference 	- name of method to be run
				delay 			  	- delay in seconds before beginning
				useFrameClock		- boolean. See <getTaskTime> for explanation.
				args				- arguments for the scheduled task
			
			See Also:
			
				<scheduleTask>
		*/ 
		static public function setTimeout ( functionReference : *, delay : Number, useFrameClock : Boolean = true, ... args ) : uint
		{
			return getInstance ( ).scheduleTask ( null, functionReference, args, delay, 0, null, null, false, useFrameClock, true );
		}
		
		/*
			Function: setTimeoutInFrames
			
				Run some function with arguments after a delay specified in
				frames.
				
			Parameters:
			
				internal functionReference 	- name of method to be run
				delay 			  	- delay in frames before beginning
				args				- arguments for the scheduled task
			
			See Also:
			
				<scheduleTask>
		*/ 
		static public function setTimeoutInFrames ( functionReference : *, delay : uint, ... args ) : uint
		{
			return getInstance ( ).scheduleTask ( null, functionReference, args, delay, 0, null, null, false, true, true, true );
		}
		
		/*
			Function: scheduleTask
			
				Schedules a task to repeat.
			
			Parameters:
			
				taskId							- id of the task. Can be null for a new task.
				method							- method to be run (String or Function)
				parameters						- parameters for method
				startInSecs						- delay (in seconds) before starting
				endAfterSecs					- ends task after this amount of time
				everyXSecs						- run method every this amount
				object							- if non-null, apply the method to this object (normally use closures)
				runNow							- true for running right now otherwise won't run till the next frame at the earliest.
				useFrameClock					- boolean. See <getTaskTime>.
				doNotPrependStatusArguments		- if false, the method is called with taskId, runningForXSecs and endInXSecs as the first three arguments
				useFrames						- if true all times are in frames instead of seconds
			
			Returns:
			
				The taskId assigned to this task.
		*/
		
		public function scheduleTask ( taskId : *, method : *, parameters : Array = null, startInSecs : * = 0, endAfterSecs : * = null, everyXSecs : * = null, object : * = null, runNow : Boolean = false, useFrameClock : Boolean = true, doNotPrependStatusArguments : Boolean = true, useFrames : Boolean = false ) : uint
		{
			if ( taskId == null || taskIndexes [ taskId ] == null )
			{
				taskId = nextTaskId++;
				taskIndexes [ taskId ] = tasks.length;
	
				var newTask : Object = {
					startTime: 						null,
					nextTime: 						null,
					endTime: 						null,
					deltaTime: 						null,		// So that the first call has runningForXSecs == 0
					pausedAt:						null,
					skewSeconds:					0,			// Always added to runningForXSecs. Convenient for replacing one transition with another.
					taskId: 						taskId,
					startInSecs:					startInSecs,
					endAfterSecs:					endAfterSecs,
					everyXSecs:						everyXSecs,
					object: 						object,
					method: 						method,
					parameters: 					parameters,
					useFrameClock:					useFrameClock ? true : false,
					doNotPrependStatusArguments:	doNotPrependStatusArguments,
					useFrames:						useFrames,
					removed:						false
				};
				
				tasks [ taskIndexes [ taskId ] ] = newTask;
				resetTask ( taskId );
				
				if ( runNow )
				{
					runningNewTaskId = taskId;
					runTasks ( taskId );
					runningNewTaskId = 0;
				}
		
				enableScheduler ( true );
			}
			
			return taskId;
		}
		
		/*
			Function: getTask
			
				Gets a task.
			
			Parameters:
			
				taskId - Id of desired task.
			
			Returns:
			
				Task (if found) or null.
		*/
		public function getTask ( taskId : * ) : Object
		{
			var task : Object = null;
			
			if ( taskId != null )
			{
				var index : * = taskIndexes [ taskId ];
				if ( index != null ) task = tasks [ index ];
			}
			
			return task;
		}
		
		/*
			Function: resetTask
			
				Resets a task of a given id which is to say it resets its
				timers. If no such task exists, does nothing.
				
			Parameters:
			
				taskId - Id of the task to reset.
		*/
		public function resetTask ( taskId : * ) : void
		{
			var task : Object = getTask ( taskId );
			
			if ( task != null )
			{
				task [ 'startTime' ] = getTaskTime ( task ) + task [ 'startInSecs' ] * unitConversion ( task );
				task [ 'nextTime'  ] = task [ 'startTime' ];
				
				if ( task [ 'endAfterSecs' ] != null )
				{
					task [ 'endTime' ] = task [ 'startTime' ] + task [ 'endAfterSecs' ] * unitConversion ( task );
				}
				
				task [ 'deltaTime' ] = null;
			}
		}
		
		/*
			Function: pauseTask
			
				Pause a task that is running.
				
			Parameters:
			
				taskId - Id of the task to pause.
				
			Returns:
			
				True if task was successfully paused; false otherwise.
		*/
		public function pauseTask ( taskId : * ) : Boolean
		{
			var paused : Boolean = false;
			
			var task : Object = getTask ( taskId );
			
			if ( task != null )
			{
				if ( task.pausedAt == null )
				{
					task.pausedAt = getTaskTime ( task );
					paused = true;
				}
			}
			
			return paused;
		}
		
		/*
			Function: resumeTask
			
				Resumes a paused task.
				
			Parameters:
			
				taskId - Id of the task to be resumed.
				
			Returns:
			
				true if task was found and successfully resumed; false otherwise.
		*/
		public function resumeTask ( taskId : * ) : Boolean
		{
			var resumed : Boolean = false;
			
			var task : Object = getTask ( taskId );
			
			if ( task != null )
			{
				if ( task.pausedAt != null )
				{
					var pauseTime : Number = getTaskTime ( task ) - task.pausedAt;
					
					task.pausedAt   = null;
					task.startTime += pauseTime;
					task.nextTime  += pauseTime;
					
					if ( task.endTime != null )
					{
						task.endTime += pauseTime;
					}
					
					resumed = true;
				}
			}
			
			return resumed;
		}
		
		/*
			Function: removeTask
			
				Remove a task from the task manager.
			
			Parameters:
			
				taskId - Id of the task to be removed.
				
			Returns:
			
				An invalid task id (0).
		*/
		public function removeTask ( taskId : * ) : uint
		{
			if ( taskId == null && runningNewTaskId > 0 )
			{
				// The behavior is to run a task as soon as it is scheduled. This
				// means that while it is running, its id has not yet been returned
				// to the caller and so if the caller calls removeTask during the
				// initial run of the task, the id will be null. This test then is
				// to handle that case.
				//
				
				taskId = runningNewTaskId;
			}
			
			var task : Object = getTask ( taskId );
			
			if ( task != null ) task.removed = true;
			
			return 0;
		}
		
		/* 
			Function: skewTaskTime
			
				Will shift the task's runningForXSecs by skewSeconds. Does nothing if no such task exists.
			
			Parameters:
		
				taskId 		- Id of the task to skew.
				skewSeconds - number of seconds to skew the task's timers by.
		*/
		/**            
		 * @private
		 */
		internal function skewTaskTime ( taskId : *, skewSeconds : Number ) : void
		{
			var task : Object = getTask ( taskId );
			
			if ( task != null ) task.skewSeconds = skewSeconds;
		}
		
		/*
			Function: getTaskRunningTime
			
				Fetches the running time of a task.
				
			Parameters:
			
				taskId - Id of the task we want the running time of.
				
			Returns:
			
				The running time of this task, in seconds.
				Can also return null if task does not exist, or
				0 if task.deltaTime does not exist.
		*/
		public function getTaskRunningTime ( taskId : * ) : *
		{
			var task : Object = getTask ( taskId );
			
			if ( task != null )
			{
				if ( task.deltaTime == null )
				{
					return 0;
				}
				else
				{
					var now : Number = getTaskTime ( task );
					
					return ( now - task.startTime - task.deltaTime ) / unitConversion ( task ) + task.skewSeconds;
				}
			}
			else
			{
				return null;
			}
		}
		
		/*
			Function: runTasks
			
				Handle the queue of scheduled tasks. By passing in a task id, we
				limit the cehck to the given task.
				
			Parameters:
			
				taskId - filter the tasks checked to this one
		*/		
		/**            
		 * @private
		 */
		internal function runTasks ( taskId : * = null ) : void
		{
			for each ( var task : Object in tasks )
			{
				var now : Number = getTaskTime ( task );
				
				if ( task.removed || ( taskId != null && task.taskId != taskId ) ) continue;
				
				// Calculate how long has passed since we reset the task and we 
				// actually got around to running the task.
				//
				
				if ( task.deltaTime == null ) task.deltaTime = now - ( task.startTime - task.startInSecs * unitConversion ( task ) );
				
				if ( task.pausedAt == null && now >= task.nextTime + task.deltaTime )
				{
					// Parameters:
					//
					//   taskId
					//   runningForXSecs
					//   endInXSecs
					//   custom parameters
					//
					
					var taskParameters : Array = [ task.taskId, ( now - task.startTime - task.deltaTime ) / unitConversion ( task ) + task.skewSeconds, task.endTime ? ( task.endTime + task.deltaTime - now ) / unitConversion ( task ) - task.skewSeconds : null ];
	
					if ( task.parameters ) taskParameters = taskParameters.concat ( task.parameters );
					
					if ( task.method is Function )
					{
						task.method.apply ( task.object, task.doNotPrependStatusArguments ? task.parameters : taskParameters );
					}
					else
					{
						task.object [ task.method ].apply ( task.object, task.doNotPrependStatusArguments ? task.parameters : taskParameters );
					}
									
					if ( task.endTime == null || task.endTime + task.deltaTime - task.skewSeconds * unitConversion ( task ) > now )
					{
						if ( task.everyXSecs != null )
						{
							task.nextTime += task.everyXSecs * unitConversion ( task );
						}
					}
					else
					{
						removeTask ( task.taskId );
					}
				}
			}
			
			// Clean up removed tasks from our tasks array.
			//
			var newTasks : Array = [ ];
			for ( var i : uint = 0; i < tasks.length; i++ ) if ( ! tasks [ i ].removed ) newTasks.push ( tasks [ i ] );
			tasks = newTasks;
			taskIndexes = { };
			for ( i = 0; i < tasks.length; i++ ) taskIndexes [ tasks [ i ].taskId ] = i;
			if ( tasks.length == 0 ) enableScheduler ( false );
			
			frameCount++;
		}
		
		public function enableScheduler ( value : Boolean ) : void
		{
			if ( value )
			{
				addListener ( DocumentManager.getInstance ( ).movie.stage, Event.ENTER_FRAME, onEnterFrame );
			}
			else
			{
				removeListener ( DocumentManager.getInstance ( ).movie.stage, Event.ENTER_FRAME, onEnterFrame );
			}
		}
		
		/**            
		 * @private
		 */
		internal function onEnterFrame ( e : Event ) : void
		{
			runTasks ( );
		}
	}
}

